/******************************************************************************
 * Qwt Widget Library
 * Copyright (C) 1997   Josef Wilgen
 * Copyright (C) 2002   Uwe Rathmann
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the Qwt License, Version 1.0
 *****************************************************************************/

#include "qwt_thermo.h"
#include "qwt_scale_draw.h"
#include "qwt_scale_map.h"
#include "qwt_color_map.h"
#include "qwt_math.h"

#include <qpainter.h>
#include <qevent.h>
#include <qdrawutil.h>
#include <qstyle.h>
#include <qstyleoption.h>
#include <qmargins.h>

#include <algorithm>
#include <functional>

static inline void qwtDrawLine(QPainter *painter, int pos, const QColor &color,
                               const QRect &pipeRect, const QRect &liquidRect,
                               Qt::Orientation orientation)
{
  painter->setPen(color);
  if (orientation == Qt::Horizontal)
  {
    if (pos >= liquidRect.left() && pos < liquidRect.right())
      painter->drawLine(pos, pipeRect.top(), pos, pipeRect.bottom());
  }
  else
  {
    if (pos >= liquidRect.top() && pos < liquidRect.bottom())
      painter->drawLine(pipeRect.left(), pos, pipeRect.right(), pos);
  }
}

static QVector<double> qwtTickList(const QwtScaleDiv &scaleDiv)
{
  QVector<double> values;

  double lowerLimit = scaleDiv.interval().minValue();
  double upperLimit = scaleDiv.interval().maxValue();

  if (upperLimit < lowerLimit)
    qSwap(lowerLimit, upperLimit);

  values += lowerLimit;

  for (int tickType = QwtScaleDiv::MinorTick;
       tickType < QwtScaleDiv::NTickTypes; tickType++)
  {
    const QList<double> ticks = scaleDiv.ticks(tickType);

    for (int i = 0; i < ticks.count(); i++)
    {
      const double v = ticks[i];
      if (v > lowerLimit && v < upperLimit)
        values += v;
    }
  }

  values += upperLimit;

  return values;
}

class QwtThermo::PrivateData
{
public:
  PrivateData()
    : orientation(Qt::Vertical)
    , scalePosition(QwtThermo::TrailingScale)
    , spacing(3)
    , borderWidth(2)
    , pipeWidth(10)
    , alarmLevel(0.0)
    , alarmEnabled(false)
    , autoFillPipe(true)
    , originMode(QwtThermo::OriginMinimum)
    , origin(0.0)
    , colorMap(NULL)
    , value(0.0)
  {
    rangeFlags = QwtInterval::IncludeBorders;
  }

  ~PrivateData() { delete colorMap; }

  Qt::Orientation orientation;
  QwtThermo::ScalePosition scalePosition;

  int spacing;
  int borderWidth;
  int pipeWidth;

  QwtInterval::BorderFlags rangeFlags;
  double alarmLevel;
  bool alarmEnabled;
  bool autoFillPipe;
  QwtThermo::OriginMode originMode;
  double origin;

  QwtColorMap *colorMap;

  double value;
};

/*!
   Constructor
   \param parent Parent widget
 */
QwtThermo::QwtThermo(QWidget *parent)
  : QwtAbstractScale(parent)
{
  m_data = new PrivateData;

  QSizePolicy policy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
  if (m_data->orientation == Qt::Vertical)
    policy.transpose();

  setSizePolicy(policy);

  setAttribute(Qt::WA_WState_OwnSizePolicy, false);
  layoutThermo(true);
}

//! Destructor
QwtThermo::~QwtThermo()
{
  delete m_data;
}

/*!
   \brief Exclude/Include min/max values

   According to the flags minValue() and maxValue()
   are included/excluded from the pipe. In case of an
   excluded value the corresponding tick is painted
   1 pixel off of the pipeRect().

   F.e. when a minimum
   of 0.0 has to be displayed as an empty pipe the minValue()
   needs to be excluded.

   \param flags Range flags
   \sa rangeFlags()
 */
void QwtThermo::setRangeFlags(QwtInterval::BorderFlags flags)
{
  if (m_data->rangeFlags != flags)
  {
    m_data->rangeFlags = flags;
    update();
  }
}

/*!
   \return Range flags
   \sa setRangeFlags()
 */
QwtInterval::BorderFlags QwtThermo::rangeFlags() const
{
  return m_data->rangeFlags;
}

/*!
   Set the current value.

   \param value New Value
   \sa value()
 */
void QwtThermo::setValue(double value)
{
  if (m_data->value != value)
  {
    m_data->value = value;
    update();
  }
}

//! Return the value.
double QwtThermo::value() const
{
  return m_data->value;
}

/*!
   \brief Set a scale draw

   For changing the labels of the scales, it
   is necessary to derive from QwtScaleDraw and
   overload QwtScaleDraw::label().

   \param scaleDraw ScaleDraw object, that has to be created with
                   new and will be deleted in ~QwtThermo() or the next
                   call of setScaleDraw().
 */
void QwtThermo::setScaleDraw(QwtScaleDraw *scaleDraw)
{
  setAbstractScaleDraw(scaleDraw);
  layoutThermo(true);
}

/*!
   \return the scale draw of the thermo
   \sa setScaleDraw()
 */
const QwtScaleDraw *QwtThermo::scaleDraw() const
{
  return static_cast<const QwtScaleDraw *>(abstractScaleDraw());
}

/*!
   \return the scale draw of the thermo
   \sa setScaleDraw()
 */
QwtScaleDraw *QwtThermo::scaleDraw()
{
  return static_cast<QwtScaleDraw *>(abstractScaleDraw());
}

/*!
   Paint event handler
   \param event Paint event
 */
void QwtThermo::paintEvent(QPaintEvent *event)
{
  QPainter painter(this);
  painter.setClipRegion(event->region());

  QStyleOption opt;
  opt.initFrom(this);
  style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);

  const QRect tRect = pipeRect();

  if (!tRect.contains(event->rect()))
  {
    if (m_data->scalePosition != QwtThermo::NoScale)
      scaleDraw()->draw(&painter, palette());
  }

  const int bw = m_data->borderWidth;

  const QBrush brush = palette().brush(QPalette::Base);
  qDrawShadePanel(&painter, tRect.adjusted(-bw, -bw, bw, bw), palette(), true,
                  bw, m_data->autoFillPipe ? &brush : NULL);

  drawLiquid(&painter, tRect);
}

/*!
   Resize event handler
   \param event Resize event
 */
void QwtThermo::resizeEvent(QResizeEvent *event)
{
  Q_UNUSED(event);
  layoutThermo(false);
}

/*!
   Qt change event handler
   \param event Event
 */
void QwtThermo::changeEvent(QEvent *event)
{
  switch (event->type())
  {
    case QEvent::StyleChange:
    case QEvent::FontChange: {
      layoutThermo(true);
      break;
    }
    default:
      break;
  }
}

/*!
   Recalculate the QwtThermo geometry and layout based on
   pipeRect() and the fonts.

   \param update_geometry notify the layout system and call update
         to redraw the scale
 */
void QwtThermo::layoutThermo(bool update_geometry)
{
  const QRect tRect = pipeRect();
  const int bw = m_data->borderWidth + m_data->spacing;
  const bool inverted = (upperBound() < lowerBound());

  int from, to;

  if (m_data->orientation == Qt::Horizontal)
  {
    from = tRect.left();
    to = tRect.right();

    if (m_data->rangeFlags & QwtInterval::ExcludeMinimum)
    {
      if (inverted)
        to++;
      else
        from--;
    }
    if (m_data->rangeFlags & QwtInterval::ExcludeMaximum)
    {
      if (inverted)
        from--;
      else
        to++;
    }

    if (m_data->scalePosition == QwtThermo::TrailingScale)
    {
      scaleDraw()->setAlignment(QwtScaleDraw::TopScale);
      scaleDraw()->move(from, tRect.top() - bw);
    }
    else
    {
      scaleDraw()->setAlignment(QwtScaleDraw::BottomScale);
      scaleDraw()->move(from, tRect.bottom() + bw);
    }

    scaleDraw()->setLength(qMax(to - from, 0));
  }
  else // Qt::Vertical
  {
    from = tRect.top();
    to = tRect.bottom();

    if (m_data->rangeFlags & QwtInterval::ExcludeMinimum)
    {
      if (inverted)
        from--;
      else
        to++;
    }
    if (m_data->rangeFlags & QwtInterval::ExcludeMaximum)
    {
      if (inverted)
        to++;
      else
        from--;
    }

    if (m_data->scalePosition == QwtThermo::LeadingScale)
    {
      scaleDraw()->setAlignment(QwtScaleDraw::RightScale);
      scaleDraw()->move(tRect.right() + bw, from);
    }
    else
    {
      scaleDraw()->setAlignment(QwtScaleDraw::LeftScale);
      scaleDraw()->move(tRect.left() - bw, from);
    }

    scaleDraw()->setLength(qMax(to - from, 0));
  }

  if (update_geometry)
  {
    updateGeometry();
    update();
  }
}

/*!
   \return Bounding rectangle of the pipe ( without borders )
          in widget coordinates
 */
QRect QwtThermo::pipeRect() const
{
  int mbd = 0;
  if (m_data->scalePosition != QwtThermo::NoScale)
  {
    int d1, d2;
    scaleDraw()->getBorderDistHint(font(), d1, d2);
    mbd = qMax(d1, d2);
  }
  const int bw = m_data->borderWidth;
  const int scaleOff = bw + mbd;

  const QRect cr = contentsRect();

  QRect pipeRect = cr;
  if (m_data->orientation == Qt::Horizontal)
  {
    pipeRect.adjust(scaleOff, 0, -scaleOff, 0);

    if (m_data->scalePosition == QwtThermo::TrailingScale)
      pipeRect.setTop(cr.top() + cr.height() - bw - m_data->pipeWidth);
    else
      pipeRect.setTop(bw);

    pipeRect.setHeight(m_data->pipeWidth);
  }
  else // Qt::Vertical
  {
    pipeRect.adjust(0, scaleOff, 0, -scaleOff);

    if (m_data->scalePosition == QwtThermo::LeadingScale)
      pipeRect.setLeft(bw);
    else
      pipeRect.setLeft(cr.left() + cr.width() - bw - m_data->pipeWidth);

    pipeRect.setWidth(m_data->pipeWidth);
  }

  return pipeRect;
}

/*!
   \brief Set the orientation.
   \param orientation Allowed values are Qt::Horizontal and Qt::Vertical.

   \sa orientation(), scalePosition()
 */
void QwtThermo::setOrientation(Qt::Orientation orientation)
{
  if (orientation == m_data->orientation)
    return;

  m_data->orientation = orientation;

  if (!testAttribute(Qt::WA_WState_OwnSizePolicy))
  {
    QSizePolicy sp = sizePolicy();
    sp.transpose();
    setSizePolicy(sp);

    setAttribute(Qt::WA_WState_OwnSizePolicy, false);
  }

  layoutThermo(true);
}

/*!
   \return Orientation
   \sa setOrientation()
 */
Qt::Orientation QwtThermo::orientation() const
{
  return m_data->orientation;
}

/*!
   \brief Change how the origin is determined.
   \sa originMode(), serOrigin(), origin()
 */
void QwtThermo::setOriginMode(OriginMode m)
{
  if (m == m_data->originMode)
    return;

  m_data->originMode = m;
  update();
}

/*!
   \return Mode, how the origin is determined.
   \sa setOriginMode(), serOrigin(), origin()
 */
QwtThermo::OriginMode QwtThermo::originMode() const
{
  return m_data->originMode;
}

/*!
   \brief Specifies the custom origin.

   If originMode is set to OriginCustom this property controls where the
   liquid starts.

   \param origin New origin level
   \sa setOriginMode(), originMode(), origin()
 */
void QwtThermo::setOrigin(double origin)
{
  if (origin == m_data->origin)
    return;

  m_data->origin = origin;
  update();
}

/*!
   \return Origin of the thermo, when OriginCustom is enabled
   \sa setOrigin(), setOriginMode(), originMode()
 */
double QwtThermo::origin() const
{
  return m_data->origin;
}

/*!
   \brief Change the position of the scale
   \param scalePosition Position of the scale.

   \sa ScalePosition, scalePosition()
 */
void QwtThermo::setScalePosition(ScalePosition scalePosition)
{
  if (m_data->scalePosition == scalePosition)
    return;

  m_data->scalePosition = scalePosition;

  if (testAttribute(Qt::WA_WState_Polished))
    layoutThermo(true);
}

/*!
   \return Scale position.
   \sa setScalePosition()
 */
QwtThermo::ScalePosition QwtThermo::scalePosition() const
{
  return m_data->scalePosition;
}

//! Notify a scale change.
void QwtThermo::scaleChange()
{
  layoutThermo(true);
}

/*!
   Redraw the liquid in thermometer pipe.
   \param painter Painter
   \param pipeRect Bounding rectangle of the pipe without borders
 */
void QwtThermo::drawLiquid(QPainter *painter, const QRect &pipeRect) const
{
  painter->save();
  painter->setClipRect(pipeRect, Qt::IntersectClip);
  painter->setPen(Qt::NoPen);

  const QwtScaleMap scaleMap = scaleDraw()->scaleMap();

  QRect liquidRect = fillRect(pipeRect);

  if (m_data->colorMap != NULL)
  {
    const QwtInterval interval = scaleDiv().interval().normalized();

    // Because the positions of the ticks are rounded
    // we calculate the colors for the rounded tick values

    QVector<double> values = qwtTickList(scaleDraw()->scaleDiv());

    if (scaleMap.isInverting())
      std::sort(values.begin(), values.end(), std::greater<double>());
    else
      std::sort(values.begin(), values.end(), std::less<double>());

    int from;
    if (!values.isEmpty())
    {
      from = qRound(scaleMap.transform(values[0]));
      qwtDrawLine(painter, from, m_data->colorMap->color(interval, values[0]),
                  pipeRect, liquidRect, m_data->orientation);
    }

    for (int i = 1; i < values.size(); i++)
    {
      const int to = qRound(scaleMap.transform(values[i]));

      for (int pos = from + 1; pos < to; pos++)
      {
        const double v = scaleMap.invTransform(pos);

        qwtDrawLine(painter, pos, m_data->colorMap->color(interval, v),
                    pipeRect, liquidRect, m_data->orientation);
      }

      qwtDrawLine(painter, to, m_data->colorMap->color(interval, values[i]),
                  pipeRect, liquidRect, m_data->orientation);

      from = to;
    }
  }
  else
  {
    if (!liquidRect.isEmpty() && m_data->alarmEnabled)
    {
      const QRect r = alarmRect(liquidRect);
      if (!r.isEmpty())
      {
        painter->fillRect(r, palette().brush(QPalette::Highlight));
        liquidRect = QRegion(liquidRect).subtracted(r).boundingRect();
      }
    }

    painter->fillRect(liquidRect, palette().brush(QPalette::ButtonText));
  }

  painter->restore();
}

/*!
   \brief Change the spacing between pipe and scale

   A spacing of 0 means, that the backbone of the scale is below
   the pipe.

   The default setting is 3 pixels.

   \param spacing Number of pixels
   \sa spacing();
 */
void QwtThermo::setSpacing(int spacing)
{
  if (spacing <= 0)
    spacing = 0;

  if (spacing != m_data->spacing)
  {
    m_data->spacing = spacing;
    layoutThermo(true);
  }
}

/*!
   \return Number of pixels between pipe and scale
   \sa setSpacing()
 */
int QwtThermo::spacing() const
{
  return m_data->spacing;
}

/*!
   Set the border width of the pipe.
   \param width Border width
   \sa borderWidth()
 */
void QwtThermo::setBorderWidth(int width)
{
  if (width <= 0)
    width = 0;

  if (width != m_data->borderWidth)
  {
    m_data->borderWidth = width;
    layoutThermo(true);
  }
}

/*!
   \return Border width of the thermometer pipe.
   \sa setBorderWidth()
 */
int QwtThermo::borderWidth() const
{
  return m_data->borderWidth;
}

/*!
   \brief Assign a color map for the fill color

   \param colorMap Color map
   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
void QwtThermo::setColorMap(QwtColorMap *colorMap)
{
  if (colorMap != m_data->colorMap)
  {
    delete m_data->colorMap;
    m_data->colorMap = colorMap;
  }
}

/*!
   \return Color map for the fill color
   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
QwtColorMap *QwtThermo::colorMap()
{
  return m_data->colorMap;
}

/*!
   \return Color map for the fill color
   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
const QwtColorMap *QwtThermo::colorMap() const
{
  return m_data->colorMap;
}

/*!
   \brief Change the brush of the liquid.

   Changes the QPalette::ButtonText brush of the palette.

   \param brush New brush.
   \sa fillBrush(), QWidget::setPalette()
 */
void QwtThermo::setFillBrush(const QBrush &brush)
{
  QPalette pal = palette();
  pal.setBrush(QPalette::ButtonText, brush);
  setPalette(pal);
}

/*!
   \return Liquid ( QPalette::ButtonText ) brush.
   \sa setFillBrush(), QWidget::palette()
 */
QBrush QwtThermo::fillBrush() const
{
  return palette().brush(QPalette::ButtonText);
}

/*!
   \brief Specify the liquid brush above the alarm threshold

   Changes the QPalette::Highlight brush of the palette.

   \param brush New brush.
   \sa alarmBrush(), QWidget::setPalette()

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
void QwtThermo::setAlarmBrush(const QBrush &brush)
{
  QPalette pal = palette();
  pal.setBrush(QPalette::Highlight, brush);
  setPalette(pal);
}

/*!
   \return Liquid brush ( QPalette::Highlight ) above the alarm threshold.
   \sa setAlarmBrush(), QWidget::palette()

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
QBrush QwtThermo::alarmBrush() const
{
  return palette().brush(QPalette::Highlight);
}

/*!
   Specify the alarm threshold.

   \param level Alarm threshold
   \sa alarmLevel()

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
void QwtThermo::setAlarmLevel(double level)
{
  m_data->alarmLevel = level;
  m_data->alarmEnabled = 1;
  update();
}

/*!
   \return Alarm threshold.
   \sa setAlarmLevel()

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
double QwtThermo::alarmLevel() const
{
  return m_data->alarmLevel;
}

/*!
   Change the width of the pipe.

   \param width Width of the pipe
   \sa pipeWidth()
 */
void QwtThermo::setPipeWidth(int width)
{
  if (width > 0)
  {
    m_data->pipeWidth = width;
    layoutThermo(true);
  }
}

/*!
   \return Width of the pipe.
   \sa setPipeWidth()
 */
int QwtThermo::pipeWidth() const
{
  return m_data->pipeWidth;
}

/*!
   \brief Enable or disable the alarm threshold
   \param on true (disabled) or false (enabled)

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
void QwtThermo::setAlarmEnabled(bool on)
{
  m_data->alarmEnabled = on;
  update();
}

/*!
   \return True, when the alarm threshold is enabled.

   \warning The alarm threshold has no effect, when
           a color map has been assigned
 */
bool QwtThermo::alarmEnabled() const
{
  return m_data->alarmEnabled;
}

/*!
   \return the minimum size hint
   \sa minimumSizeHint()
 */
QSize QwtThermo::sizeHint() const
{
  return minimumSizeHint();
}

/*!
   \return Minimum size hint
   \warning The return value depends on the font and the scale.
   \sa sizeHint()
 */
QSize QwtThermo::minimumSizeHint() const
{
  int w = 0, h = 0;

  if (m_data->scalePosition != NoScale)
  {
    const int sdExtent = qwtCeil(scaleDraw()->extent(font()));
    const int sdLength = scaleDraw()->minLength(font());

    w = sdLength;
    h = m_data->pipeWidth + sdExtent + m_data->spacing;
  }
  else // no scale
  {
    w = 200;
    h = m_data->pipeWidth;
  }

  if (m_data->orientation == Qt::Vertical)
    qSwap(w, h);

  w += 2 * m_data->borderWidth;
  h += 2 * m_data->borderWidth;

  // finally add the margins
  const QMargins m = contentsMargins();
  w += m.left() + m.right();
  h += m.top() + m.bottom();

  return QSize(w, h);
}

/*!
   \brief Calculate the filled rectangle of the pipe

   \param pipeRect Rectangle of the pipe
   \return Rectangle to be filled ( fill and alarm brush )

   \sa pipeRect(), alarmRect()
 */
QRect QwtThermo::fillRect(const QRect &pipeRect) const
{
  double origin;
  if (m_data->originMode == OriginMinimum)
  {
    origin = qMin(lowerBound(), upperBound());
  }
  else if (m_data->originMode == OriginMaximum)
  {
    origin = qMax(lowerBound(), upperBound());
  }
  else // OriginCustom
  {
    origin = m_data->origin;
  }

  const QwtScaleMap scaleMap = scaleDraw()->scaleMap();

  int from = qRound(scaleMap.transform(m_data->value));
  int to = qRound(scaleMap.transform(origin));

  if (to < from)
    qSwap(from, to);

  QRect fillRect = pipeRect;
  if (m_data->orientation == Qt::Horizontal)
  {
    fillRect.setLeft(from);
    fillRect.setRight(to);
  }
  else // Qt::Vertical
  {
    fillRect.setTop(from);
    fillRect.setBottom(to);
  }

  return fillRect.normalized();
}

/*!
   \brief Calculate the alarm rectangle of the pipe

   \param fillRect Filled rectangle in the pipe
   \return Rectangle to be filled with the alarm brush

   \sa pipeRect(), fillRect(), alarmLevel(), alarmBrush()
 */
QRect QwtThermo::alarmRect(const QRect &fillRect) const
{
  QRect alarmRect(0, 0, -1, -1); // something invalid

  if (!m_data->alarmEnabled)
    return alarmRect;

  const bool inverted = (upperBound() < lowerBound());

  bool increasing;
  if (m_data->originMode == OriginCustom)
  {
    increasing = m_data->value > m_data->origin;
  }
  else
  {
    increasing = m_data->originMode == OriginMinimum;
  }

  const QwtScaleMap map = scaleDraw()->scaleMap();
  const int alarmPos = qRound(map.transform(m_data->alarmLevel));
  const int valuePos = qRound(map.transform(m_data->value));

  if (m_data->orientation == Qt::Horizontal)
  {
    int v1, v2;
    if (inverted)
    {
      v1 = fillRect.left();

      v2 = alarmPos - 1;
      v2 = qMin(v2, increasing ? fillRect.right() : valuePos);
    }
    else
    {
      v1 = alarmPos + 1;
      v1 = qMax(v1, increasing ? fillRect.left() : valuePos);

      v2 = fillRect.right();
    }
    alarmRect.setRect(v1, fillRect.top(), v2 - v1 + 1, fillRect.height());
  }
  else
  {
    int v1, v2;
    if (inverted)
    {
      v1 = alarmPos + 1;
      v1 = qMax(v1, increasing ? fillRect.top() : valuePos);

      v2 = fillRect.bottom();
    }
    else
    {
      v1 = fillRect.top();

      v2 = alarmPos - 1;
      v2 = qMin(v2, increasing ? fillRect.bottom() : valuePos);
    }
    alarmRect.setRect(fillRect.left(), v1, fillRect.width(), v2 - v1 + 1);
  }

  return alarmRect;
}

#if QWT_MOC_INCLUDE
#  include "moc_qwt_thermo.cpp"
#endif
